Skip to content

fix: harden middlecache downloads and consume OpenAPI archive - #33085

Open
mvvmm wants to merge 8 commits into
productionfrom
consume-openapi-archive
Open

fix: harden middlecache downloads and consume OpenAPI archive#33085
mvvmm wants to merge 8 commits into
productionfrom
consume-openapi-archive

Conversation

@mvvmm

@mvvmm mvvmm commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes intermittent production build failures around the 24MB openapi.json fetched from middlecache during the Astro prerender. Two root causes:

  1. Silent corruption via brotli — with default Accept-Encoding, middlecache served on-the-fly brotli (no integrity checksum); a corrupted/truncated transfer silently decompressed to garbage, failing the build with Bad control character in string literal in JSON.
  2. Concurrency racegetSchema downloaded lazily during prerender; pages render in parallel, so concurrent downloads raced on the shared .tmp file and a retry's cleanup could delete a sibling call's freshly-written file (ENOENT on openapi.json).

Changes:

  • bin/fetch-openapi.ts (new, mirrors the skills flow) — runs from the prebuild/predev hooks, downloads the gzip-compressed openapi.tar.gz from middlecache (extracting + parsing as the integrity check), falling back to raw openapi.json. --soft for predev.
  • getSchema now just reads the local .tmp/.../openapi.json during prerender — no network, no race.
  • downloadToDotTempIfNotPresent requests Accept-Encoding: identity (no brotli), writes atomically (temp file + rename), checks response.ok + Content-Length, retries up to 3×, dedups concurrent same-destination downloads, and accepts an optional validate callback.
  • getDotTmpPath resolves the repo root by walking up to package.json, so tsx prebuild and the bundled prerender (which resolve import.meta.url to different depths) agree on .tmp.
  • Adds a shared extractTarGz helper (with stripComponents) and unifies bin/fetch-skills.ts onto it.
  • Unit tests: retry, validation, size mismatch, existing-file handling, tar extraction, and concurrent-download dedup.

A companion change in the middlecache pipeline publishes the openapi.tar.gz archive (already merged); the prebuild falls back to raw openapi.json if it is absent.

Verified: full pnpm run build succeeds (8932 pages), pnpm run check, ESLint, Prettier, and 136/136 tests pass.

The 24MB openapi.json was fetched over HTTP during the build. With
default Accept-Encoding, middlecache served on-the-fly brotli, which has
no integrity checksum; a corrupted/truncated transfer silently
decompressed to garbage, intermittently failing the build with
"Bad control character in string literal in JSON".

- downloadToDotTempIfNotPresent now requests identity encoding (no
  brotli), writes atomically via a temp file, checks response.ok and
  Content-Length, and retries up to 3 times. An optional validate
  callback makes the whole download+validate unit retryable.
- getSchema now prefers the gzip-compressed openapi.tar.gz from
  middlecache (extracted with tar, validated by gzip CRC32 + JSON.parse
  before use), falling back to the raw openapi.json.
- Adds unit tests covering retry, validation, size mismatch, and
  existing-file handling.
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

This pull request requires reviews from CODEOWNERS as it changes files that match the following patterns:

Pattern Owners
*.ts @cloudflare/content-engineering, @kodster28
package.json @cloudflare/content-engineering
/src/util/api.ts @cloudflare/content-engineering, @kodster28

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 28, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://consume-openapi-archive.previews.developers.cloudflare.com (commit fa057e7)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://60eb615a.previews.developers.cloudflare.com fa057e7 2026-08-28T18:53:44.609Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://e709655c.previews.developers.cloudflare.com b1de7eb 2026-08-28T18:37:22.833Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://8f23c8e1.previews.developers.cloudflare.com a797a58 2026-08-28T17:34:52.491Z Visit the dashboard ↗
  • Build: Failed ❌

View logs ↗
ff28527 2026-08-28T16:51:11.963Z View logs ↗
  • Build: In progress 🔵

View logs ↗
49e6891 2026-08-28T16:41:22.993Z View logs ↗
  • Build: Failed ❌

View logs ↗
c4e3921 2026-08-28T16:23:05.077Z View logs ↗
  • Build: In progress 🔵

View logs ↗
3655976 2026-08-28T15:38:29.464Z View logs ↗

bin/fetch-skills.ts had its own inline tar spawn; extractTarGz (shared
with the OpenAPI archive path) now supports stripComponents and
fetch-skills uses it instead. Adds extractTarGz unit tests.
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

mvvmm added 3 commits August 28, 2026 11:41
The build failed intermittently with ENOENT on openapi.json because
getSchema downloaded the schema lazily during prerender. Pages render in
parallel, so concurrent downloads raced on the shared .tmp file — a
retry's cleanup could delete a sibling call's freshly-written file.

Mirror the skills flow: a new bin/fetch-openapi.ts (run from the
prebuild/predev hooks) downloads the gzip-compressed openapi.tar.gz from
middlecache and extracts openapi.json to .tmp before the build starts.
getSchema now just reads the local file.

- bin/fetch-openapi.ts: download archive (fall back to raw openapi.json),
  extract + parse as the integrity check; --soft for predev
- package.json: prebuild/predev run fetch-openapi after fetch-skills
- api.ts: getSchema reads .tmp/.../openapi.json, single-flighted
- custom-loaders.ts: getDotTmpPath now resolves the repo root by walking
  up to package.json (tsx prebuild and the bundled prerender resolve
  import.meta.url to different depths); downloadToDotTempIfNotPresent
  dedups concurrent same-destination downloads
- tests: concurrent-download dedup + retry-after-failure coverage

Verified: full `pnpm run build` succeeds (8932 pages), check/lint/tests
pass.
Match bin/fetch-skills.ts behavior: print a skip message and exit when
the extracted openapi.json is already present, with --force to re-fetch.
Workers Builds invokes `pnpm run build:incremental`, which skipped the
`prebuild` hook, so `bin/fetch-openapi.ts` never ran and prerendering
failed with ENOENT on the schema file. Add a `prebuild:incremental` hook
mirroring `prebuild` (fetch-skills + fetch-openapi) and make `getSchema`
read-only so a build invoked without the pre-step fails loudly instead of
silently downloading mid-render.

Also factor the middlecache fetch/extract logic into `src/util/openapi-schema.ts`
shared by `bin/fetch-openapi.ts` and `getSchema`.
@mvvmm
mvvmm marked this pull request as ready for review August 28, 2026 17:41
@mvvmm
mvvmm requested review from a team and kodster28 as code owners August 28, 2026 17:41
@cloudflare-docs-bot

cloudflare-docs-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Review

✅ No issues found in commit fa057e7.

Code Review

This code review is in beta and may not always be helpful — use your judgment.

No code review issues found.

Conventions

No convention issues found.

Style Guide Review

No style-guide issues found.

Commands

Only codeowners can run commands. Post a comment with the command to trigger it.

Command Description
/review Runs a review now. Incremental if a prior review exists, full if not.
/full-review Re-reviews the entire PR diff from scratch, ignoring incremental history. Useful after a rebase, when you want a fresh review, or if the bot gets out of sync and reports issues that no longer exist.
/ignore-review-limit Permanently lifts the 2-review automatic limit for this PR. Future pushes will trigger reviews as normal.
/disable-auto-review Stops automatic reviews from triggering on future pushes to this PR. Codeowners can still run /review or /full-review manually.
/rebase Rebases the PR branch against production. On conflict, attempts to resolve automatically using AI. Stops with an explanation if confidence is not high enough.

mvvmm added 3 commits August 28, 2026 12:59
- custom-loaders: reject tar members with `..` segments or absolute
  paths (zip-slip); reject on spawn `error` instead of hanging; capture
  tar stderr in extraction errors
- openapi-schema: extract to a staging dir and atomically promote on
  success, so a failed extract/parse leaves no stale files to mask a
  fresh download failure
- tests: check spawnSync status when building fixtures; add regression
  tests for unsafe tar member paths
- package.json: share the fetch-skills + fetch-openapi command via a
  single fetch:assets script used by prebuild and prebuild:incremental
Replaces the `response.body!` non-null assertion in downloadWithRetry with
an explicit check, so a bodyless response (e.g. 204/HEAD) fails with a
clear error instead of an unhelpful TypeError from Readable.fromWeb(null).
Adds a regression test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants